package cn.wolfcode.service.impl;

import cn.wolfcode.common.exception.BusinessException;
import cn.wolfcode.common.util.AssertUtils;
import cn.wolfcode.common.web.Result;
import cn.wolfcode.domain.*;
import cn.wolfcode.feign.IntegralFeignApi;
import cn.wolfcode.feign.PaymentFeignApi;
import cn.wolfcode.mapper.OrderInfoMapper;
import cn.wolfcode.mapper.PayLogMapper;
import cn.wolfcode.mapper.RefundLogMapper;
import cn.wolfcode.mq.DefaultSendCallback;
import cn.wolfcode.mq.MQConstant;
import cn.wolfcode.mq.OrderMessage;
import cn.wolfcode.redis.SeckillRedisKey;
import cn.wolfcode.service.IOrderInfoService;
import cn.wolfcode.service.ISeckillProductService;
import cn.wolfcode.common.util.IdGenerateUtil;
import cn.wolfcode.web.msg.SeckillCodeMsg;
import com.alibaba.fastjson.JSON;
import io.seata.spring.annotation.GlobalTransactional;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.rocketmq.client.producer.LocalTransactionState;
import org.apache.rocketmq.client.producer.TransactionSendResult;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;
import java.util.Date;

/**
 * Created by wolfcode
 */
@Slf4j
@Service
@CacheConfig(cacheNames = "orders")
public class OrderInfoServiceImpl implements IOrderInfoService {

    @Autowired
    private ISeckillProductService seckillProductService;

    @Autowired
    private OrderInfoMapper orderInfoMapper;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Autowired
    private PayLogMapper payLogMapper;

    @Autowired
    private RefundLogMapper refundLogMapper;

    @Autowired
    private RocketMQTemplate rocketMQTemplate;

    @Autowired
    private PaymentFeignApi paymentFeignApi;

    @Autowired
    private IntegralFeignApi integralFeignApi;

    @Override
    public OrderInfo selectByUserIdAndSeckillId(Long userId, Long seckillId, Integer time) {
        return orderInfoMapper.selectByUserIdAndSeckillId(userId, seckillId, time);
    }

    @Override
    public OrderInfo selectByOrderNo(String orderNo) {
        String jsonOrderInfo = stringRedisTemplate.opsForValue().get("orders::detail:" + orderNo);
        if (StringUtils.isNotEmpty(jsonOrderInfo)) {
            OrderInfo orderInfo = JSON.parseObject(jsonOrderInfo, OrderInfo.class);
            return orderInfo;
        }
        return orderInfoMapper.find(orderNo);
    }


    @Override
    @Transactional(rollbackFor = Exception.class)
    public OrderInfo doSeckill(Long phone, SeckillProductVo vo) {
        // 1. 扣除秒杀商品库存
        seckillProductService.decrStockCount(vo.getId());
        // 2. 创建秒杀订单并保存
        OrderInfo orderInfo = this.buildOrderInfo(phone, vo);
        orderInfoMapper.insert(orderInfo);
        // 3. 返回订单编号
        return orderInfo;
    }

    @CachePut(key = "'detail:' + #result.orderNo")
    @Override
    @Transactional(rollbackFor = Exception.class)
    public OrderInfo doSeckill(Long phone, Long seckillId, Integer time) {
        SeckillProductVo sp = seckillProductService.selectByIdAndTime(seckillId, time);
        return this.doSeckill(phone, sp);
    }

    @Override
    public void failedRollback(OrderMessage message) {
        // 回补Redis库存
        this.rollbackRedisStock(message.getSeckillId(), message.getTime());
        // 删除用户下单标识
        String userOrderKey = SeckillRedisKey.SECKILL_STOCK_COUNT_HASH.join(message.getSeckillId() + "");
        stringRedisTemplate.opsForHash().delete(userOrderKey, message.getUserPhone() + "");
        // 删除本地标识 通过mq发送广播消息 让每一个实例都清空自己的map
        rocketMQTemplate.asyncSend(MQConstant.CANCEL_SECKILL_OVER_SIGE_TOPIC, message.getSeckillId(), new DefaultSendCallback("取消本地标识"));
    }

    /**
     * 回补Redis库存
     */
    private void rollbackRedisStock(Long seckillId, Integer time) {
        // 回补Redis数量控制
        Long stockCount = seckillProductService.selectStockCountById(seckillId);
        String hashKey = SeckillRedisKey.SECKILL_STOCK_COUNT_HASH.join(time + "");
        stringRedisTemplate.opsForHash().put(hashKey, seckillId + "", stockCount + "");
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void checkPayTimeout(OrderMessage message) {
        // 1.基于订单编号查询订单对象
        // 2.判断状态如果不等于已支付就说明取消
        int row = orderInfoMapper.changePayStatus(message.getOrderNo(), OrderInfo.STATUS_CANCEL, OrderInfo.PAY_TYPE_ONLINE);
        if (row > 0) {
            // 3.mysql秒杀商品数量+1
            seckillProductService.incrStockCount(message.getSeckillId());
            // 4.失败订单信息回滚(redis库存回补 & 删除用户下单标识 & 删除本地标识)
            this.failedRollback(message);
        }
    }

    @Override
    public String onlinePay(String orderNo) {
        // 1.基于订单号,查询订单对象
        OrderInfo orderInfo = this.selectByOrderNo(orderNo);
        // 2.判断订单状态是否为未支付状态，只有未支付才允许发起支付
        AssertUtils.isTrue(orderInfo.getStatus().equals(OrderInfo.STATUS_ARREARAGE), "订单状态异常，无法发起支付");
        // 3.封装支付参数
        PayVo payVo = new PayVo();
        payVo.setBody("seckill" + orderInfo.getProductName());
        payVo.setSubject(orderInfo.getProductName());
        payVo.setOutTradeNo(orderNo);
        payVo.setTotalAmount(orderInfo.getSeckillPrice().toString());

        // 4.远程调用支付服务
        Result<String> result = paymentFeignApi.prePay(payVo);
        return result.checkAndGet();
    }

    @Override
    public void alipaySuccess(PayResult payResult) {
        // 1. 获取订单信息，判断订单是否存在
        OrderInfo orderInfo = this.selectByOrderNo(payResult.getOutTradeNo());
        AssertUtils.notNull(orderInfo, "订单信息有误");

        // 2. 判断订单状态是否正确
        AssertUtils.isTrue(orderInfo.getStatus().equals(OrderInfo.STATUS_ARREARAGE), "订单状态错误");

        // 3. 判断订单支付金额是否正确
        AssertUtils.isTrue(orderInfo.getSeckillPrice().equals(new BigDecimal(payResult.getTotalAmount())), "");

        // 4.更新订单支付成功状态
        int row = orderInfoMapper.changePayStatus(payResult.getOutTradeNo(), OrderInfo.STATUS_ACCOUNT_PAID, OrderInfo.PAY_TYPE_ONLINE);
        AssertUtils.isTrue(row > 0, "订单状态修改失败");

        // 5.记录支付日志
        PayLog payLog = new PayLog();
        payLog.setPayType(PayLog.PAY_TYPE_ONLINE);
        payLog.setTotalAmount(payResult.getTotalAmount());
        payLog.setOutTradeNo(payResult.getOutTradeNo());
        payLog.setTradeNo(payResult.getTradeNo());
        payLog.setNotifyTime(System.currentTimeMillis() + "");
        payLogMapper.insert(payLog);
    }

    /**
     * 退款
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void refund(String orderNo) {
        // 1.基于orderNo查询订单对象
        OrderInfo orderInfo = orderInfoMapper.find(orderNo);
        // 2.判断订单状态是否为已支付
        AssertUtils.isTrue(OrderInfo.STATUS_ACCOUNT_PAID.equals(orderInfo.getStatus()), "订单状态错误");
        // 3.判断支付类型 根据不同支付类型调用不同退款接口
        Result<Boolean> result = null;
        // 封装退款对象
        RefundVo refundVo = new RefundVo(orderNo, orderInfo.getSeckillPrice().toString(), "");
        if (orderInfo.getPayType() == OrderInfo.PAY_TYPE_ONLINE) {
            // 支付宝退款
            result = paymentFeignApi.refund(refundVo);
        } else {
            // 积分退款 ：发送RocketMQ事务消息
            // result = integralFeignApi.refund(refundVo);
            log.info("[积分退款] 准备发送积分退款事务消息{}", JSON.toJSONString(refundVo));
            Message<RefundVo> message = MessageBuilder.withPayload(refundVo)
                    .setHeader("orderNo", orderNo)
                    .build();
            TransactionSendResult txSendResult = rocketMQTemplate.sendMessageInTransaction(MQConstant.INTEGRAL_REFUND_TX_GROUP,
                    MQConstant.INTEGRAL_REFUND_TX_TOPIC, message, orderNo);
            if (txSendResult.getLocalTransactionState().equals(LocalTransactionState.COMMIT_MESSAGE) ||
                    txSendResult.getLocalTransactionState().equals(LocalTransactionState.UNKNOW)) {
                log.info("[积分退款] 积分退款本地事务执行成功，等待远程服务执行状态：{}", txSendResult.getLocalTransactionState());
                return;
            }
            // 退款失败
            throw new BusinessException(SeckillCodeMsg.REFUND_ERROR);
        }
        // 判断是否退款成功 如果成功继续下一步
        if (result == null || result.hasError() || !result.getData()) {
            throw new BusinessException(SeckillCodeMsg.REFUND_ERROR);
        }
        this.refundRollback(orderInfo);
    }


    private void refundRollback(OrderInfo orderInfo) {
        // 4.更新订单状态为已退款
        int row = orderInfoMapper.changeRefundStatus(orderInfo.getOrderNo(), OrderInfo.STATUS_REFUND);
        AssertUtils.isTrue(row > 0, "退款失败，更新订单状态异常");
        // 5.库存回补(Mysql&Redis)
        seckillProductService.incrStockCount(orderInfo.getSeckillId());
        this.rollbackRedisStock(orderInfo.getSeckillId(), orderInfo.getSeckillTime());

        // 6.删除本地下单标识
        rocketMQTemplate.asyncSend(MQConstant.CANCEL_SECKILL_OVER_SIGE_TOPIC, orderInfo.getSeckillId(), new DefaultSendCallback("取消本地标识"));
        // 7.退款日志记录
        RefundLog refundLog = new RefundLog();
        refundLog.setRefundReason("用户申请退款" + orderInfo.getProductName());
        refundLog.setRefundTime(new Date());
        refundLog.setRefundType(orderInfo.getPayType());
        refundLog.setRefundAmount(orderInfo.getSeckillPrice().toString());
        refundLog.setOutTradeNo(orderInfo.getOrderNo());
        refundLogMapper.insert(refundLog);
    }

    /**
     * 积分支付
     */
    @Override
    //@Transactional(rollbackFor = Exception.class)
    @GlobalTransactional
    public void integralPay(String orderNo, Long phone) {
        // 1.基于订单号,查询订单对象
        OrderInfo orderInfo = this.selectByOrderNo(orderNo);
        // 2.判断订单状态是否为未支付状态，只有未支付才允许发起支付
        AssertUtils.isTrue(orderInfo.getStatus().equals(OrderInfo.STATUS_ARREARAGE), "订单状态异常，无法发起支付");

        //判断当前用户是否是创建这个订单的用户
        AssertUtils.isTrue(orderInfo.getUserId().equals(phone), "非法操作");

        // 3.封装支付参数
        OperateIntegralVo integralVo = new OperateIntegralVo();
        integralVo.setInfo("积分支付" + orderInfo.getProductName());
        integralVo.setValue(orderInfo.getIntergral());
        integralVo.setUserId(phone);
        integralVo.setOutTradeNo(orderNo);

        // 4.远程调用支付服务
        Result<String> result = integralFeignApi.prePay(integralVo);
        String tradeNo = result.checkAndGet();

        // 5.更新订单状态为支付成功状态
        int row = orderInfoMapper.changePayStatus(orderNo, OrderInfo.STATUS_ACCOUNT_PAID, OrderInfo.PAY_TYPE_INTERGRAL);
        AssertUtils.isTrue(row > 0, "订单状态修改失败");

        // 6.记录支付流水日志
        PayLog payLog = new PayLog();
        payLog.setPayType(PayLog.PAY_TYPE_INTERGRAL);
        payLog.setTotalAmount(integralVo.getValue() + "");
        payLog.setOutTradeNo(orderNo);
        // 积分账户流水变动交易号
        payLog.setTradeNo(tradeNo);
        payLog.setNotifyTime(System.currentTimeMillis() + "");
        payLogMapper.insert(payLog);
    }

    @Override
    public void integralRefundRollback(String orderNo) {
        OrderInfo orderInfo = orderInfoMapper.find(orderNo);
        this.refundRollback(orderInfo);
    }

    @Override
    public RefundLog selectRefundLogByOrderNo(String orderNo) {
        return refundLogMapper.selectByOrderNo(orderNo);
    }

    private OrderInfo buildOrderInfo(Long phone, SeckillProductVo vo) {
        Date now = new Date();
        OrderInfo orderInfo = new OrderInfo();
        orderInfo.setCreateDate(now);
        orderInfo.setDeliveryAddrId(1L);
        orderInfo.setIntergral(vo.getIntergral());
        orderInfo.setOrderNo(IdGenerateUtil.get().nextId() + "");
        orderInfo.setPayType(OrderInfo.PAY_TYPE_ONLINE);
        orderInfo.setProductCount(1);
        orderInfo.setProductId(vo.getProductId());
        orderInfo.setProductImg(vo.getProductImg());
        orderInfo.setProductName(vo.getProductName());
        orderInfo.setProductPrice(vo.getProductPrice());
        orderInfo.setSeckillDate(now);
        orderInfo.setSeckillId(vo.getId());
        orderInfo.setSeckillPrice(vo.getSeckillPrice());
        orderInfo.setSeckillTime(vo.getTime());
        orderInfo.setStatus(OrderInfo.STATUS_ARREARAGE);
        orderInfo.setUserId(phone);
        return orderInfo;
    }
}
